package cn.tedu.charging.cost.service.impl;

import cn.tedu.chaging.common.pojo.param.ChargingProcessParam;
import cn.tedu.charging.cost.dao.repository.CostRuleCacheRepository;
import cn.tedu.charging.cost.dao.repository.CostRuleRepository;
import cn.tedu.charging.cost.pojo.ChargingData;
import cn.tedu.charging.cost.pojo.ChargingDataTime;
import cn.tedu.charging.cost.pojo.po.CostRulePO;
import cn.tedu.chaging.common.pojo.vo.ChargingProcessVO;
import cn.tedu.charging.cost.service.CostService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import java.math.BigDecimal;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Slf4j
@Service
public class CostServiceImpl implements CostService {

    /**
     * 充电历史记录
     * 1 判断是否是第一次或者是第N次
     * 2 保存每次的充电记录,为了做累加
     * key 订单编号  我们的计价要给多个订单计价
     *
     * chargingProcessData 是个本地的map 重启后数据没了
     * 是否能无限的put数据
     * 同时有一百万个人来下单  chargingProcessData 里存 100W条订单数据
     * 我们在java里写程序,无限的创建对象,会占用内存空间
     * jvm的内存满了,放不下数据了 OutMemoryException 内存溢出
     *
     * 换一种场景
     * chargingProcessData = select xxx from xx   假定xx表里有1亿条数据
     *
     * while(true) {
     *    new XXX();
     * }
     *
     * 计价服务 1台 C1               用户A 订单号1  map 创建了订单
     * 计价服务 3台 C1 C2 C3         用户A 订单号1
     *
     * 不能用本地的map chargingProcessData 要换成全局的数据存储 es
     *
     */
    private Map<String, ChargingData>  chargingProcessData = new HashMap<String,ChargingData>();

    @Autowired
    private CostRuleCacheRepository cacheRepository;

    @Autowired
    private CostRuleRepository costRuleRepository;

    /**
     * 计算电费
     * @param chargingProcessParam
     * @return
     */
    @Override
    public ChargingProcessVO calculateCost(ChargingProcessParam chargingProcessParam) {
        String orderNo = chargingProcessParam.getOrderNo();

        //   1*1 = 1    2* 1 =2
        //1 如果是第一次 直接计算价格
        //2 如果不是第一次,总价 = 上次计算的价格 + 当前的价格  价格的累加
        //A 如何计算是第几次
        //B 如何需要保存上传计算的价格

        //通过订单编号获取上次充电记录
        log.debug("通过订单编号获取上次充电记录:orderNo:{}",orderNo);
        ChargingData chargingData = chargingProcessData.get(orderNo);
        log.debug("通过订单编号获取上次充电记录:orderNo:{},chargingData:{}", orderNo, chargingData);
        if (chargingData == null) {
            log.info("没有取到充电数据,说明是第一次获取 orderNo:{}", orderNo);
            //没有取到,说明是第一次同步充电数据
            chargingData = new ChargingData();
            chargingData.setOrderNo(orderNo);
            chargingData.setUserId(chargingProcessParam.getUserId());
            chargingData.setStartTime(LocalDateTime.now());
            chargingProcessData.put(orderNo, chargingData);
        }else {
            log.debug("取到充电数据 不是第一次 orderNo:{},chargingData:{}", orderNo, chargingData);
            //取到,说明不是第一次同步充电数据
            BigDecimal totalCost = chargingData.getTotalCost();

        }
        //价格计算
        //时间段 不同价格不同
        // 12:00-14:00 高峰  2 14:00:15:00 平峰 1.5  15:00-16:00 低峰 1    4个小时
        //区域  1    5
        //电池 总度数  10   1000
        //枪的类型  1   5    5000
        //不同的区域在不同的时间段,不同的枪类型 单度电的价格是不同的
        Integer stationId = chargingProcessParam.getStationId();
        log.debug("获取计价规则:入参:{}",stationId);
        List<CostRulePO> costRules = getCostRule(stationId);
        log.debug("获取计价规则:入参:{},结果:{}", stationId,costRules);
        //获取当前时间 小时 通过当前小时 来计算属于哪个时间段 尖 峰 平 谷 决定价格
        LocalDateTime now = LocalDateTime.now();
        //当前小时
        int hour = now.getHour();
        //循环当前计价规则列表
        for (CostRulePO costRule : costRules) {
            //判断当前时间同步的充电数据在哪个时段
            // 当前时间 大于等于开始时间 并且 小于等于结束时间
            if (hour >= costRule.getStartTime() && hour < costRule.getEndTime()){
                //当前时段每度电的价格
                BigDecimal powerFee = costRule.getPowerFee();
                //每度电的价格存到充电状态 chargingData
                chargingData.setCurrentOneFee(powerFee);

                //取电量
                //判断是不是第一次
                if (chargingData.getLastChargingProcessParam() == null) {
                    //第一次上传充电进度
                    //获取充电度数
                    float chargingCapacity = chargingProcessParam.getChargingCapacity();
                    //把 float 类型 充电度数 chargingCapacity  转为 BigDecimal 类型 chargingCapacityBigDecimal
                    BigDecimal chargingCapacityBigDecimal = new BigDecimal(Float.toString(chargingCapacity));
                    //每度电的价格 * 充电度数
                    BigDecimal chargingCost = powerFee.multiply(chargingCapacityBigDecimal);
                    log.debug("当前时间:{},计费名称:{},每度电价格是:{}元,充电量:{},当前花费:{},总花费:{}",hour,costRule.getName(),powerFee,chargingCapacity,chargingCost,chargingCost);
                    //设置本次充电费用到充电数据对象 chargingData
                    chargingData.setTotalCost(chargingCost);
                    //设置本次充电的记录到充电数据对象
                    chargingData.setLastChargingProcessParam(chargingProcessParam);
                }else {
                    //取上次充电的记录
                    ChargingProcessParam prviousChargingProcessParam = chargingData.getLastChargingProcessParam();
                    //做累加
                    //1:3  2:1  3:2  每次实际充了多少度 需要记录每次充了多少  3+1+2 = 6
                    //1:3  2:4  3:6  每次已经充了多少度 只需记录已经充了多少
                    //上次充电度数
                    float prviousChargingCapacity = prviousChargingProcessParam.getChargingCapacity();
                    //最新充电度数
                    float currentChargingCapacity = chargingProcessParam.getChargingCapacity();
                    //当前实际充电度数
                    float realChargingCapacity = currentChargingCapacity - prviousChargingCapacity;
                    BigDecimal realChargingCapacityBigDecimal = new BigDecimal(Float.toString(realChargingCapacity));
                    //每度电的价格 * 充电度数
                    BigDecimal currentChargingCost = powerFee.multiply(realChargingCapacityBigDecimal);
                    //取出上次的花费 + 本次花费 = 目前花费  累加
                    BigDecimal totalCost = chargingData.getTotalCost().add(currentChargingCost);
                    //设置目前花费 到 chargingData
                    log.debug("当前时间:{},计费名称:{},每度电价格是:{}元,充电量:{},当前花费:{},总花费:{}",
                            hour, costRule.getName(),powerFee,
                            realChargingCapacity,currentChargingCost,totalCost);
                    chargingData.setTotalCost(totalCost);
                    chargingData.setLastChargingProcessParam(chargingProcessParam);
                }
            }
        }
        //计算时长 为什么给用户显示充电时长,从业务上讲 不返回ok,给用户安全感,用户知道钱花到什么地方,会更安心\
        //充电开始时间 和 当前时间
        ChargingProcessVO chargingProcessVO = buildChargingProcessVO(chargingProcessParam, chargingData);
        return chargingProcessVO;
    }

    /**
     * 计算充电时长
     * @param chargingData
     * @return
     */
    private  ChargingDataTime getChargingDateTime(ChargingData chargingData) {
        Duration between = Duration.between(chargingData.getStartTime(), LocalDateTime.now());
        long hours = between.toHours();
        long minutes = between.toMinutes();
        long seconds = between.getSeconds();
        return new ChargingDataTime(hours,minutes,seconds);
    }

    /**
     * 通过最后一次同步的数据获取价格相关信息
     * @param chargingProcessParam
     * @param chargingData
     * @return
     */
    private ChargingProcessVO buildChargingProcessVO(ChargingProcessParam chargingProcessParam,
                                                     ChargingData chargingData) {
        ChargingProcessVO chargingProcessVO = new ChargingProcessVO();
        BeanUtils.copyProperties(chargingProcessParam,chargingProcessVO);
        //设置每度电的价格
        chargingProcessVO.setCurrentOneFee(chargingData.getCurrentOneFee());
        //计算充电时长
        ChargingDataTime chargingDateTime = getChargingDateTime(chargingData);
        chargingProcessVO.setHours(chargingDateTime.getHours());
        chargingProcessVO.setMinutes(chargingDateTime.getMinutes());
        chargingProcessVO.setSeconds(chargingDateTime.getSeconds());
        chargingProcessVO.setTotalCost(chargingData.getTotalCost());
        return chargingProcessVO;
    }

    @Override
    public ChargingProcessVO getOrderCost(String orderNo) {
        ChargingData chargingData = chargingProcessData.get(orderNo);
        //chargingData 里 LastChargingProcessParam 是最后一次同步的数据
        //LastChargingProcessParam 有价格的累加 和 时间的累加
        ChargingProcessParam lastChargingProcessParam = chargingData.getLastChargingProcessParam();
        return buildChargingProcessVO(lastChargingProcessParam,chargingData);
    }



    /**
     * 缓存和数据库配合使用方法 cache-aside
     * 通过充电站id获取计价规则
     * @param stationId
     * @return
     */
    private List<CostRulePO> getCostRule(Integer stationId) {
        //通过充电站id去  <<redis缓存>>  中获取计价规则
        List<CostRulePO> cacheCostRulePO =  cacheRepository.getCostRuleByStationId(stationId);
        log.debug("从redis缓存获取数据:{}", cacheCostRulePO);
        //判断 <<redis缓存>> 计价规则是否为空
        if (cacheCostRulePO == null) {
            //判断缓存中计价规则 为空
            //通过充电站id去 <<数据库>> 中获取计价规则
            List<CostRulePO> costRulePOs  = costRuleRepository.getCostRuleByStationId(stationId);
            //判断 <<数据库>> 计价规则是否为空
            if (!CollectionUtils.isEmpty(costRulePOs)) {
                // <<数据库>> 计价规则不为空 保存计价规则到  <<redis缓存>>
                cacheRepository.saveCostRule(stationId,costRulePOs);
                //返回计价规则
                log.debug("从数据库获取数据:{}", costRulePOs);
                return costRulePOs;
            }else {
                log.warn("未找到计价规则,告警告警,停止充电");
            }
        }
        return cacheCostRulePO;
    }
}
